/*
 * Copyright (C) 2019 xuexiangjys(xuexiangjys@163.com)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

package com.xuexiang.xui_lib.component.imageview.edit;

import ohos.agp.components.AttrSet;
import ohos.agp.components.Component;
import ohos.agp.render.BlendMode;
import ohos.agp.render.Canvas;
import ohos.agp.render.Paint;
import ohos.agp.render.Path;
import ohos.agp.render.PixelMapHolder;
import ohos.agp.render.Texture;
import ohos.agp.utils.Color;
import ohos.agp.utils.RectFloat;
import ohos.app.Context;
import ohos.media.image.PixelMap;
import ohos.media.image.common.PixelFormat;
import ohos.media.image.common.Size;
import ohos.multimodalinput.event.MmiPoint;
import ohos.multimodalinput.event.TouchEvent;
import ohos.utils.Pair;

import java.util.Stack;

/**
 * 画板，自定义绘图视图，用于在用户触摸事件上进行绘图
 *
 * @author xuexiang
 * @since 2019-10-28 9:52
 */
public class BrushDrawingView extends Component {
    static final float DEFAULT_BRUSH_SIZE = 25.0f;
    static final float DEFAULT_ERASER_SIZE = 50.0f;
    static final int DEFAULT_OPACITY = 255;

    private float mBrushSize = DEFAULT_BRUSH_SIZE;
    private float mBrushEraserSize = DEFAULT_ERASER_SIZE;
    private int mOpacity = DEFAULT_OPACITY;

    private final Stack<LinePath> mDrawnPaths = new Stack<>();
    private final Stack<LinePath> mRedoPaths = new Stack<>();
    private Paint mDrawPaint;
    private Paint canvasPaint;
    private Canvas mDrawCanvas;
    private boolean mBrushDrawMode;
    private Path mPath;
    private float mTouchX, mTouchY;
    private static final float TOUCH_TOLERANCE = 4;
    private BrushViewChangeListener mBrushViewChangeListener;
    private PixelMap canvasPixelMap;
    private int screenWidth, screenHeight;// 屏幕長寬
    private Texture texture;
    private RectFloat rectFloat;
    private int paintColor;

    public BrushDrawingView(Context context) {
        super(context);
        setupBrushDrawing();
    }

    public BrushDrawingView(Context context, AttrSet attrSet) {
        super(context, attrSet);
        setupBrushDrawing();
    }

    public BrushDrawingView(Context context, AttrSet attrSet, String styleName) {
        super(context, attrSet, styleName);
        setupBrushDrawing();
    }

    public BrushDrawingView(Context context, AttrSet attrSet, int resId) {
        super(context, attrSet, resId);
        setupBrushDrawing();
    }

    private void setupBrushDrawing() {
        //Caution: This line is to disable hardware acceleration to make eraser feature work properly
        setupPathAndPaint();
        init();
        setVisibility(Component.HIDE);
    }

    private void setupPathAndPaint() {
        mPath = new Path();
        mDrawPaint = new Paint();
        mDrawPaint.setAntiAlias(true);
        mDrawPaint.setDither(true);
        mDrawPaint.setColor(new Color(paintColor));
        mDrawPaint.setStyle(Paint.Style.STROKE_STYLE);
        mDrawPaint.setStrokeJoin(Paint.Join.ROUND_JOIN);
        mDrawPaint.setStrokeCap(Paint.StrokeCap.ROUND_CAP);
        mDrawPaint.setStrokeWidth(mBrushSize);
        mDrawPaint.setAlpha(mOpacity);
        mDrawPaint.setBlendMode(BlendMode.SRC_OVER);
        canvasPaint = new Paint();
        canvasPaint.setDither(true);
    }

    private void refreshBrushDrawing() {
        mBrushDrawMode = true;
        setupPathAndPaint();
    }

    void brushEraser() {
        mBrushDrawMode = true;
        mDrawPaint.setBlendMode(BlendMode.CLEAR);
        mDrawPaint.setStrokeWidth(mBrushEraserSize);
    }

    void setBrushDrawingMode(boolean brushDrawMode) {
        mBrushDrawMode = brushDrawMode;
        if (brushDrawMode) {
            this.setVisibility(Component.VISIBLE);
            refreshBrushDrawing();
        }
    }

    void setOpacity(@IntRange(from = 0, to = 255) int opacity) {
        this.mOpacity = opacity;
        setBrushDrawingMode(true);
    }

    int getOpacity() {
        return mOpacity;
    }

    boolean getBrushDrawingMode() {
        return mBrushDrawMode;
    }

    void setBrushSize(float size) {
        mBrushSize = size;
        setBrushDrawingMode(true);
    }

    void setBrushColor(int color) {
        paintColor = color;
        //mDrawPaint.setColor(new Color(color));
        setBrushDrawingMode(true);
    }

    void setBrushEraserSize(float brushEraserSize) {
        this.mBrushEraserSize = brushEraserSize;
        setBrushDrawingMode(true);
    }

    void setBrushEraserColor(int color) {
        mDrawPaint.setColor(new Color(color));
        setBrushDrawingMode(true);
    }

    float getEraserSize() {
        return mBrushEraserSize;
    }

    float getBrushSize() {
        return mBrushSize;
    }

    int getBrushColor() {
        return mDrawPaint.getColor().getValue();
    }

    void clearAll() {
        mDrawnPaths.clear();
        mRedoPaths.clear();
        if (mDrawCanvas != null) {
            mDrawCanvas.drawColor(0, Canvas.PorterDuffMode.CLEAR);
        }
        invalidate();
    }

    void setBrushViewChangeListener(BrushViewChangeListener brushViewChangeListener) {
        mBrushViewChangeListener = brushViewChangeListener;
    }

    private boolean isVisibility() {
        return getVisibility() == VISIBLE;
    }

    private void init() {
        setLayoutRefreshedListener(new LayoutRefreshedListener() {
            @Override
            public void onRefreshed(Component component) {
                System.out.println(component.getWidth() + "==onRefreshed");
                if (screenWidth != component.getWidth() || screenHeight != component.getHeight()) {
                    onSizeChanged(component.getWidth(), component.getHeight());
                }
            }
        });
        addDrawTask(drawRect());
        setTouchEventListener(new TouchEventListener() {
            @Override
            public boolean onTouchEvent(Component component, TouchEvent touchEvent) {
                return doTouchEvent(touchEvent);
            }
        });
    }

    @Override
    public void addDrawTask(DrawTask task) {
        super.addDrawTask(task);
        task.onDraw(this, mCanvasForTaskOverContent);
    }

    //抽象出drawRect方法，以便复用
    private DrawTask drawRect() {
        //DrawTask
        DrawTask drawRect = new DrawTask() {
            @Override
            public void onDraw(Component component, Canvas canvas) {
                if (isVisibility()) {
                    doDrawTask(canvas);
                }
            }
        };
        return drawRect;
    }

    protected void onSizeChanged(int w, int h) {
        screenWidth = w;
        screenHeight = h;
        PixelMap.InitializationOptions opt = new PixelMap.InitializationOptions();
        opt.pixelFormat = PixelFormat.ARGB_8888;
        opt.size = new Size(w, h);
        opt.editable = true;
        canvasPixelMap = PixelMap.create(opt);
        texture = new Texture(canvasPixelMap);
        rectFloat = new RectFloat(0, 0, screenWidth, screenHeight);
        mDrawCanvas = new Canvas(texture);
        for (LinePath linePath : mDrawnPaths) {
            mDrawCanvas.drawPath(linePath.getDrawPath(), linePath.getDrawPaint());
        }
    }

    protected void doDrawTask(Canvas canvas) {
        int saveLayer = canvas.saveLayer(rectFloat, canvasPaint);
        canvas.drawPixelMapHolder(new PixelMapHolder(canvasPixelMap), 0, 0, canvasPaint);
        canvas.drawPath(mPath, mDrawPaint);
        canvas.drawTexture(0, 0, texture);
        canvas.restoreToCount(saveLayer);
    }

    public PixelMap getCanvasPixelMap() {
        return canvasPixelMap;
    }

    /**
     * Handle touch event to draw paint on canvas i.e brush drawing
     *
     * @param event points having touch info
     * @return true if handling touch events
     */
    public boolean doTouchEvent(TouchEvent event) {
        if (mBrushDrawMode) {
            MmiPoint m = event.getPointerScreenPosition(0);
            int[] location = getLocationOnScreen();
            float touchX = m.getX() - location[0];
            float touchY = m.getY() - location[1];
            switch (event.getAction()) {
                case TouchEvent.PRIMARY_POINT_DOWN:
                    touchStart(touchX, touchY);
                    break;
                case TouchEvent.POINT_MOVE:
                    touchMove(touchX, touchY);
                    break;
                case TouchEvent.PRIMARY_POINT_UP:
                    touchUp();
                    break;
                default:
                    return false;
            }
            invalidate();
            return true;
        } else {
            return false;
        }
    }

    //后退
    boolean undo() {
        if (!mDrawnPaths.empty()) {
            mRedoPaths.push(mDrawnPaths.pop());
            onSizeChanged(screenWidth, screenHeight);
            for (LinePath linePath : mDrawnPaths) {
                mDrawCanvas.drawPath(linePath.getDrawPath(), linePath.getDrawPaint());
            }
            invalidate();
        }
        if (mBrushViewChangeListener != null) {
            mBrushViewChangeListener.onViewRemoved(this);
        }
        return !mDrawnPaths.empty();
    }

    //前进
    boolean redo() {
        if (!mRedoPaths.empty()) {
            mDrawnPaths.push(mRedoPaths.pop());
            onSizeChanged(screenWidth, screenHeight);
            for (LinePath linePath : mDrawnPaths) {
                mDrawCanvas.drawPath(linePath.getDrawPath(), linePath.getDrawPaint());
            }
            invalidate();
        }
        if (mBrushViewChangeListener != null) {
            mBrushViewChangeListener.onViewAdd(this);
        }
        return !mRedoPaths.empty();
    }

    private void touchStart(float x, float y) {
        mRedoPaths.clear();
        mPath.reset();
        mPath.moveTo(x, y);
        mTouchX = x;
        mTouchY = y;
        if (mBrushViewChangeListener != null) {
            mBrushViewChangeListener.onStartDrawing();
        }
    }

    private void touchMove(float x, float y) {
        float dx = Math.abs(x - mTouchX);
        float dy = Math.abs(y - mTouchY);
        if (dx >= TOUCH_TOLERANCE || dy >= TOUCH_TOLERANCE) {
            mPath.quadTo(mTouchX, mTouchY, (x + mTouchX) / 2, (y + mTouchY) / 2);
            mTouchX = x;
            mTouchY = y;
        }
        mPath.lineTo(mTouchX, mTouchY);
        mDrawCanvas.drawPath(mPath, mDrawPaint);
    }

    private void touchUp() {
        //  mPath.lineTo(mTouchX, mTouchY);
        // Commit the path to our offscreen
        // mDrawCanvas.drawPath(mPath, mDrawPaint);
        // kill this so we don't double draw
        mDrawnPaths.push(new LinePath(mPath, mDrawPaint));
        mPath = new Path();
        if (mBrushViewChangeListener != null) {
            mBrushViewChangeListener.onStopDrawing();
            mBrushViewChangeListener.onViewAdd(this);
        }
    }

    Paint getDrawingPaint() {
        return mDrawPaint;
    }

    Pair<Stack<LinePath>, Stack<LinePath>> getDrawingPath() {
        return new Pair<>(mDrawnPaths, mRedoPaths);
    }
}